/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  outgoingHttpHeaders,
  requestOptions,
  incomingHttpHeaders,
  secureServerOptions,
  http2Server,
  http2SecureServer,
  secureClientSessionOptions,
  ClientSessionOptions,
  Socket,
  IncomingHttpStatusHeader,
  Error
} from './typeDeclaration';

import socket from '@ohos.net.socket';
import { URL } from './url';

import { BufferClass, Buffer } from './buffer';
import hilog from '@ohos.hilog';
import Url from '@ohos.url'
import { BusinessError } from '@ohos.base';
import { JSON, stream } from '@kit.ArkTS';
import {
  assertValidPseudoHeader,
  checkIsHttpToken,
  debugSessionObj,
  getAuthority,
  getLocalIp,
  getURLOrigin,
  isNetWorkAvailable,
  mapToHeaders,
  MAX_ADDITIONAL_SETTINGS,
  validateFunction,
  validateString
} from './utils'
import { Http2Napi } from 'libhttp2_napi.so'
import { Http2Session } from './session/Http2Session';
import { ClientHttp2Session } from './session/ClientHttp2Session';
import { Http2Stream } from './stream/Http2Stream';
import {
  constants,
  expectedNGConstants,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_SCHEME,
  kAuthority,
  kBoundSession,
  kHandle,
  kID,
  kInit,
  kMaxALTSVC,
  kMaybeDestroy,
  kNativeFields,
  kNoPayloadMethods,
  kProtocol,
  kServer,
  HTTP2_METHOD_GET,
  kSession,
  kSessionPriorityListenerCount,
  kState,
  kUpdateTimer,
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_PATH,
  HTTP2_METHOD_HEAD,
  STREAM_OPTION_EMPTY_PAYLOAD,
  kSentHeaders,
  STREAM_FLAGS_HEAD_REQUEST,
  STREAM_OPTION_GET_TRAILERS,
  STREAM_FLAGS_HAS_TRAILERS,
  STREAM_FLAGS_HEADERS_SENT,
  HTTP2_HEADER_STATUS,
  expectedStatusCodes
} from './constant/Constant';
import { Settings } from './interface/Settings';
import {LogUtils} from './LogUtils';

export type IncomingHttpHeaders = incomingHttpHeaders;

export type SecureClientSessionOptions = secureClientSessionOptions

export function createSecureServer(secureServerOptions: secureServerOptions): any {
  console.log('Mocking createSecureServer with options:', secureServerOptions);
  const mockServer = {
    listen: (port, callback) => {
      console.log(`Secure server listening on port ${port}`);
      if (callback) {
        callback();
      }
    },
    close: () => {
      console.log('Secure server closed');
    }
  };
  return mockServer;
}

// When the socket emits an error, destroy the associated Http2Session and
// forward it the same error.

function socketOnClose() {
  const session = this[kBoundSession];
  if (session !== undefined) {
    debugSessionObj(session, 'socket closed');
    const err = session.connecting ? new Error('ERR_SOCKET_CLOSED') : null;
    const state = session[kState];
    state.streams.forEach((stream) => stream.close(constants.NGHTTP2_CANCEL));
    state.pendingStreams.forEach((stream) => stream.close(constants.NGHTTP2_CANCEL));
    session.close();
    session[kMaybeDestroy](err);
  }
}

// Handles the on('stream') event for a session and forwards
// it on to the server object.
function sessionOnStream(stream, headers, flags, rawHeaders) {
  if (this[kServer] !== undefined) {
    this[kServer].emit('stream', stream, headers, flags, rawHeaders);
  }
}

// ServerHttp2Session instances should never have to wait for the socket
// to connect as they are always created after the socket has already been
// established.
export class ServerHttp2Session extends Http2Session {
  constructor(options, socket, server) {
    super(expectedNGConstants.NGHTTP2_SESSION_SERVER, options, socket);
    this[kServer] = server;
    this[kNativeFields][kSessionPriorityListenerCount] =
      server ? server.listenerCount('priority') : 0;
  }

  get server() {
    return this[kServer];
  }

  altsvc(alt, originOrStream) {
    if (this.destroyed) {
      hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }

    let stream = 0;
    let origin;

    if (typeof originOrStream === 'string') {
      origin = getURLOrigin(originOrStream);
      if (origin === 'null') {
        hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
        throw new Error('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
      }
    } else if (typeof originOrStream === 'number') {
      if (originOrStream >>> 0 !== originOrStream || originOrStream === 0) {
        hilog.error(0x8691, 'ohos_grpc_node', 'ERR_OUT_OF_RANGE(\'originOrStream> 0 && < $\{2 ** 32} originOrStream');
        throw new Error(`ERR_OUT_OF_RANGE('originOrStream> 0 && < ${2**32} originOrStream`);
      }
      stream = originOrStream;
    } else if (originOrStream !== undefined) {
      // Allow origin to be passed a URL or object with origin property
      if (originOrStream !== null && typeof originOrStream === 'object') {
        origin = originOrStream.origin;
      }
      // Note: if originOrStream is an object with an origin property other
      // than a URL, then it is possible that origin will be malformed.
      // We do not verify that here. Users who go that route need to
      // ensure they are doing the right thing or the payload data will
      // be invalid.
      if (typeof origin !== 'string') {
        hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
        throw new Error(`ERR_INVALID_ARG_TYPE(originOrStream['string', 'number', 'URL', 'object'],
          originOrStream`);
      } else if (origin === 'null' || origin.length === 0) {
        hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
        throw new Error(`ERR_HTTP2_ALTSVC_INVALID_ORIGIN`);
      }
    }

    // validateString(alt, 'alt');
    // if (!kQuotedString.test(alt))
    //   throw new ERR_INVALID_CHAR('alt');

    // Max length permitted for ALTSVC
    if ((alt.length + (origin !== undefined ? origin.length : 0)) > kMaxALTSVC) {
      hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_ALTSVC_LENGTH');
      throw new Error('ERR_HTTP2_ALTSVC_LENGTH');
    }

    this[kHandle].altsvc(stream, origin || '', alt);
  }

  // Submits an origin frame to be sent.
  origin(...origins) {
    if (this.destroyed) {
      hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }

    if (origins.length === 0) {
      return;
    }

    let arr = '';
    let len = 0;
    const count = origins.length;
    for (let i = 0; i < count; i++) {
      let origin = origins[i];
      if (typeof origin === 'string') {
        origin = getURLOrigin(origin);
      } else if (origin != null && typeof origin === 'object') {
        origin = origin.origin;
      }
      validateString(origin, 'origin');
      if (origin === 'null') {
        hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_INVALID_ORIGIN');
        throw new Error('ERR_HTTP2_INVALID_ORIGIN');
      }

      arr += `${origin}\0`;
      len += origin.length;
    }

    if (len > kMaxALTSVC) {
      hilog.error(0x8691, 'ohos_grpc_node', 'ERR_HTTP2_ORIGIN_LENGTH');
      throw new Error('ERR_HTTP2_ORIGIN_LENGTH');
    }

    this[kHandle].origin(arr, count);
  }
}

// ClientHttp2Session instances have to wait for the socket to connect after
// they have been created. Various operations such as request() may be used,
// but the actual protocol communication will only occur after the socket
// has been connected.


export class ServerHttp2Stream extends Http2Stream {
  constructor(session, handle, id, options, headers) {
    super(session, options);
    handle.owner = this;
    this[kInit](id, handle);
    this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
    this[kAuthority] = getAuthority(headers);
  }

  resume() {
  };

  write(buf?: string) {
  };

  pause() {
  };

  on(event: 'response',
    listener: (headers: incomingHttpHeaders & IncomingHttpStatusHeader, flags: number) => void,
  ): void;

  on(event: 'data', callback: (data: Buffer) => void): void;

  on(event: 'error', callback: (err: Error) => void): void;

  on(event: 'trailers', callback: (headers: IncomingHttpHeaders) => void): void;

  on(event: 'end', callback: () => void): void;

  on(event: 'close', callback: () => void): void;

  on(event: 'remoteSettings', callback: (settings: Settings) => void): void;

  on(event: string, callback: (...args: any[]) => void) {

  }

  once(event: 'close', callback: () => void): void;

  once(event: 'close', callback: (session: ServerHttp2Session) => void): void;

  once(event: 'wantTrailers', callback: () => void): void;

  once(event: 'once', callback: (errorCode: number, lastStreamID: number, opaqueData?: Buffer) => void): void;

  once(event: 'goaway', callback: (errorCode: number, lastStreamID: number, opaqueData?: Buffer) => void): void;

  once(event: 'error', callback: (error) => void): void;

  once(event: string, callback: (...args: any[]) => void) {

  }

  end() {
  }

  // True if the remote peer accepts push streams
  get pushAllowed() {
    return !this.destroyed &&
      !this.closed &&
      !this.session.closed &&
      !this.session.destroyed &&
    this[kSession].remoteSettings.enablePush;
  }

  destroyed: boolean = false;

  // Create a push stream, call the given callback with the created
  // Http2Stream for the push stream.
  pushStream(headers, options, callback) {
    if (!this.pushAllowed) {
      throw new Error('ERR_HTTP2_PUSH_DISABLED');
    }

    if (this[kID] % 2 === 0) {
      throw new Error('ERR_HTTP2_NESTED_PUSH');
    }


    const session = this[kSession];

    // debugStreamObj(this, 'initiating push stream');

    this[kUpdateTimer]();

    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }

    validateFunction(callback, 'callback');

    // assertIsObject(options, 'options');
    options = { ...options };
    options.endStream = !!options.endStream;

    // assertIsObject(headers, 'headers');
    // headers = ObjectAssign({ __proto__: null }, headers);

    if (headers[HTTP2_HEADER_METHOD] === undefined) {
      headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
    }
    if (getAuthority(headers) === undefined) {
      headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
    }
    if (headers[HTTP2_HEADER_SCHEME] === undefined) {
      headers[HTTP2_HEADER_SCHEME] = this[kProtocol];
    }
    if (headers[HTTP2_HEADER_PATH] === undefined) {
      headers[HTTP2_HEADER_PATH] = '/';
    }

    let headRequest = false;
    if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
      headRequest = options.endStream = true;
    }

    const headersList = mapToHeaders(headers);

    const streamOptions = options.endStream ? STREAM_OPTION_EMPTY_PAYLOAD : 0;

    const ret = this[kHandle].pushPromise(headersList, streamOptions);
    let err;


    const id = ret.id();
    const stream = new ServerHttp2Stream(session, ret, id, options, headers);
    stream[kSentHeaders] = headers;

    stream.push(null);

    if (options.endStream) {
      stream.end();
    }

    if (headRequest) {
      stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST;
    }

    // process.nextTick(callback, null, stream, headers, 0);
  }

  // Initiate a response on this Http2Stream
  respond(headers, options) {
    if (this.destroyed || this.closed) {
      throw new Error('ERR_HTTP2_INVALID_STREAM');
    }
    if (this.headersSent) {
      throw new Error('ERR_HTTP2_HEADERS_SENT');
    }

    const state = this[kState];

    // assertIsObject(options, 'options');
    options = { ...options };

    // debugStreamObj(this, 'initiating response');
    this[kUpdateTimer]();

    options.endStream = !!options.endStream;

    let streamOptions = 0;
    if (options.endStream) {
      streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
    }

    if (options.waitForTrailers) {
      streamOptions |= STREAM_OPTION_GET_TRAILERS;
      state.flags |= STREAM_FLAGS_HAS_TRAILERS;
    }

    // headers = processHeaders(headers, options);
    // const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
    // this[kSentHeaders] = headers;

    state.flags |= STREAM_FLAGS_HEADERS_SENT;

    // Close the writable side if the endStream option is set or status
    // is one of known codes with no payload, or it's a head request
    const statusCode = headers[HTTP2_HEADER_STATUS] | 0;
    if (!!options.endStream ||
      statusCode === expectedStatusCodes.HTTP_STATUS_NO_CONTENT ||
      statusCode === expectedStatusCodes.HTTP_STATUS_RESET_CONTENT ||
      statusCode === expectedStatusCodes.HTTP_STATUS_NOT_MODIFIED ||
      this.headRequest === true) {
      options.endStream = true;
      this.end();
    }

    // const ret = this[kHandle].respond(headersList, streamOptions);
    // if (ret < 0)
    //   this.destroy(new NghttpError(ret));
  }

  // Initiate a response using an open FD. Note that there are fewer
  // protections with this approach. For one, the fd is not validated by
  // default. In respondWithFile, the file is checked to make sure it is a
  // regular file, here the fd is passed directly. If the underlying
  // mechanism is not able to read from the fd, then the stream will be
  // reset with an error code.
  respondWithFD(fd, headers, options) {
    if (this.destroyed || this.closed) {
      throw new Error('ERR_HTTP2_INVALID_STREAM');
    }
    if (this.headersSent) {
      throw new Error('ERR_HTTP2_HEADERS_SENT');
    }

    const session = this[kSession];

    // assertIsObject(options, 'options');
    options = { ...options };

    if (options.offset !== undefined && typeof options.offset !== 'number') {
      throw new Error(`ERR_INVALID_ARG_VALUE('options.offset', options.offset)`);
    }

    if (options.length !== undefined && typeof options.length !== 'number') {
      throw new Error(`ERR_INVALID_ARG_VALUE('options.length', options.length)`);
    }

    if (options.statCheck !== undefined &&
      typeof options.statCheck !== 'function') {
      throw new Error(`ERR_INVALID_ARG_VALUE('options.statCheck', options.statCheck)`);
    }

    let streamOptions = 0;
    if (options.waitForTrailers) {
      streamOptions |= STREAM_OPTION_GET_TRAILERS;
      this[kState].flags |= STREAM_FLAGS_HAS_TRAILERS;
    }
  }

  // Initiate a file response on this Http2Stream. The path is passed to
  // fs.open() to acquire the fd with mode 'r', then the fd is passed to
  // fs.fstat(). Assuming fstat is successful, a check is made to ensure
  // that the file is a regular file, then options.statCheck is called,
  // giving the user an opportunity to verify the details and set additional
  // headers. If statCheck returns false, the operation is aborted and no
  // file details are sent.
  respondWithFile(path, headers, options) {

  }

  // Sends a block of informational headers. In theory, the HTTP/2 spec
  // allows sending a HEADER block at any time during a streams lifecycle,
  // but the HTTP request/response semantics defined in HTTP/2 places limits
  // such that HEADERS may only be sent *before* or *after* DATA frames.
  // If the block of headers being sent includes a status code, it MUST be
  // a 1xx informational code and it MUST be sent before the request/response
  // headers are sent, or an error will be thrown.
  additionalHeaders(headers) {
  }
}

export class http2 {
  static async connect(
    authority: string | URL,
    options?: ClientSessionOptions | SecureClientSessionOptions,
    listener?: (session: ClientHttp2Session, socket: Socket) => void
  ): Promise<ClientHttp2Session> {
    if (typeof options === 'function') {
      listener = options;
      options = undefined;
    }
    if (typeof authority === 'string') {
      authority = new Url.URL(authority);
    }

    const protocol = authority.protocol || options.protocol || 'https:';
    const port = '' + (authority.port !== '' ?
    authority.port : (authority.protocol === 'http:' ? 80 : 443));
    let host = 'localhost';

    if (authority.hostname) {
      host = authority.hostname;

      if (host[0] === '[') {
        host = host.slice(1, -1);
      }
    } else if (authority.host) {
      host = authority.host;
    }

    let mysocket;
    LogUtils.getInstance().info(`option: ${JSON.stringify(options)}`);
    LogUtils.getInstance().info(`switch.protocol: ${protocol}`);
    switch (protocol) {
      case 'http:': {
        //TODO no support http
      }
        break;
      case 'https:': {
        if (isNetWorkAvailable()) {
          let http2Client = new Http2Napi();
          await http2Client.connect(host, port);
          mysocket = http2Client;
        } else {
          console.error("HTTP2 Error Currently no network")
          throw ("HTTP2 Error Currently no network")
        }
      }
        break;
      default:
        throw new Error(`ERR_HTTP2_UNSUPPORTED_PROTOCOL(${protocol})`);
    }
    const session = new ClientHttp2Session(options, mysocket);
    session[kProtocol] = protocol;
    if (typeof listener === 'function') {
      session.once('connect', () => {
        listener(session, mysocket);
      })
    }
    return session;
  }
}













